Micron Document
C.S.Burner 🪙 ━►🔥GIT Node

Node / reticulum_mirror / MeshChatX / files / meshchatx / src / frontend / components / call / RingtoneEditorModal.vue

Displaying Raw • Download

meshchatx/src/frontend/components/call/RingtoneEditorModal.vue 9ec2a88817819d433c5d68dbec0ca070ca989b91 (9ec2a888) Text, 22.10 KB

T8b949e<!-- SPDX-License-Identifier: 0BSD -->

Tff7b72<templateTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffabsolute inset-0 bg-zinc-900/80 backdrop-blur-smTa5d6ff" @Te6edf3click=Ta5d6ff"Te6edf3$emitTa5d6ff(Ta5d6ff'Ta5d6ffcloseTa5d6ff'Ta5d6ff)Ta5d6ff"Tff7b72>Tff7b72</div>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffrelative w-full max-w-4xl bg-white dark:bg-zinc-900 rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]Ta5d6ff"
Tff7b72>
T8b949e<!-- Header -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffpx-6 py-4 border-b border-gray-100 dark:border-zinc-800 flex items-center justify-betweenTa5d6ff"Tff7b72>
Tff7b72<divTff7b72>
Tff7b72<h3 Te6edf3class=Ta5d6ff"Ta5d6fftext-lg font-bold text-gray-900 dark:text-whiteTa5d6ff"Tff7b72>Edit RingtoneTff7b72</h3>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-gray-500 dark:text-zinc-500Ta5d6ff"Tff7b72>{{ ringtone.display_name }}Tff7b72</p>
Tff7b72</div>
Tff7b72<button
Te6edf3class=Ta5d6ff"Ta5d6ffp-2 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-full transition-colorsTa5d6ff"
@Te6edf3click=Ta5d6ff"Te6edf3$emitTa5d6ff(Ta5d6ff'Ta5d6ffcloseTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffcloseTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-6 text-gray-500Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>

T8b949e<!-- Content -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1 overflow-y-auto p-6 space-y-6Ta5d6ff"Tff7b72>
T8b949e<!-- Waveform Container -->
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffrelative bg-gray-50 dark:bg-zinc-800/50 rounded-2xl p-4 border border-gray-100 dark:border-zinc-800 min-h-[200px] flex flex-col justify-centerTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffloadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col items-center justify-center space-y-3Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffsize-8 border-4 border-blue-500/20 border-t-blue-500 rounded-full animate-spinTa5d6ff"
Tff7b72>Tff7b72</div>
Tff7b72<p Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-500 dark:text-zinc-400 font-mediumTa5d6ff"Tff7b72>Loading audio...Tff7b72</p>
Tff7b72</div>

Tff7b72<div Te6edf3v-show=Ta5d6ff"Ta5d6ff!loadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffrelativeTa5d6ff"Tff7b72>
Tff7b72<canvas
Te6edf3ref=Ta5d6ff"Ta5d6ffwaveformTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full h-40 cursor-pointerTa5d6ff"
@Te6edf3mousedown=Ta5d6ff"Ta5d6ffhandleWaveformClickTa5d6ff"
Tff7b72>Tff7b72</canvas>

T8b949e<!-- Playback Progress -->
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute top-0 bottom-0 w-0.5 bg-blue-500 z-10 pointer-events-noneTa5d6ff"
Te6edf3:style=Ta5d6ff"Ta5d6ff{ left: progressPercent + Ta5d6ff'Ta5d6ff%Ta5d6ff'Ta5d6ff }Ta5d6ff"
Tff7b72>Tff7b72</div>

T8b949e<!-- Selection Overlays -->
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute top-0 bottom-0 bg-blue-500/10 border-x-2 border-blue-500 z-20Ta5d6ff"
Te6edf3:style=Ta5d6ff"Ta5d6ff{ left: startPercent + Ta5d6ff'Ta5d6ff%Ta5d6ff'Ta5d6ff, width: endPercent - startPercent + Ta5d6ff'Ta5d6ff%Ta5d6ff'Ta5d6ff }Ta5d6ff"
Tff7b72>
T8b949e<!-- Handles -->
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute top-1/2 -left-3 -translate-y-1/2 size-6 bg-white dark:bg-zinc-700 border-2 border-blue-500 rounded-full shadow-lg cursor-ew-resize flex items-center justify-center groupTa5d6ff"
@mousedown.stop.Te6edf3prevent=Ta5d6ff"Ta5d6ffstartDragging(Ta5d6ff'Ta5d6ffstartTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffw-0.5 h-3 bg-blue-500 group-hover:h-4 transition-allTa5d6ff"Tff7b72>Tff7b72</div>
Tff7b72</div>
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffabsolute top-1/2 -right-3 -translate-y-1/2 size-6 bg-white dark:bg-zinc-700 border-2 border-blue-500 rounded-full shadow-lg cursor-ew-resize flex items-center justify-center groupTa5d6ff"
@mousedown.stop.Te6edf3prevent=Ta5d6ff"Ta5d6ffstartDragging(Ta5d6ff'Ta5d6ffendTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffw-0.5 h-3 bg-blue-500 group-hover:h-4 transition-allTa5d6ff"Tff7b72>Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- Controls -->
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffgrid grid-cols-1 md:grid-cols-2 gap-6Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center justify-betweenTa5d6ff"Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-sm font-bold text-gray-700 dark:text-zinc-300Ta5d6ff"Tff7b72>Time RangeTff7b72</span>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-[10px] font-mono text-gray-500Ta5d6ff"
Tff7b72>{{ formatTime(startTime) }} - {{ formatTime(endTime) }} ({{
formatTime(endTime - startTime)
}})Tff7b72</span
>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1Ta5d6ff"Tff7b72>
Tff7b72<label Te6edf3class=Ta5d6ff"Ta5d6ffblock text-[10px] uppercase font-bold text-gray-400 mb-1Ta5d6ff"Tff7b72>StartTff7b72</label>
Tff7b72<input
v-model.Te6edf3number=Ta5d6ff"Ta5d6ffstartTimeTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffnumberTa5d6ff"
Te6edf3step=Ta5d6ff"Ta5d6ff0.01Ta5d6ff"
Te6edf3min=Ta5d6ff"Ta5d6ff0Ta5d6ff"
Te6edf3:max=Ta5d6ff"Ta5d6ffendTimeTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full bg-gray-50 dark:bg-zinc-800 border-none rounded-lg text-sm px-3 py-2 focus:ring-2 focus:ring-blue-500 text-gray-900 dark:text-whiteTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex-1Ta5d6ff"Tff7b72>
Tff7b72<label Te6edf3class=Ta5d6ff"Ta5d6ffblock text-[10px] uppercase font-bold text-gray-400 mb-1Ta5d6ff"Tff7b72>EndTff7b72</label>
Tff7b72<input
v-model.Te6edf3number=Ta5d6ff"Ta5d6ffendTimeTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffnumberTa5d6ff"
Te6edf3step=Ta5d6ff"Ta5d6ff0.01Ta5d6ff"
Te6edf3:min=Ta5d6ff"Ta5d6ffstartTimeTa5d6ff"
Te6edf3:max=Ta5d6ff"Ta5d6fftotalDurationTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffw-full bg-gray-50 dark:bg-zinc-800 border-none rounded-lg text-sm px-3 py-2 focus:ring-2 focus:ring-blue-500 text-gray-900 dark:text-whiteTa5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-col justify-endTa5d6ff"Tff7b72>
Tff7b72<button
Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center justify-center gap-2 py-3 rounded-xl font-bold transition-all w-fullTa5d6ff"
Te6edf3:class=Ta5d6ff"
Ta5d6ff isPlaying
? Ta5d6ff'Ta5d6ffbg-orange-500 text-white shadow-lg shadow-orange-500/20Ta5d6ff'
Ta5d6ff : Ta5d6ff'Ta5d6ffbg-blue-600 text-white shadow-lg shadow-blue-500/20Ta5d6ff'
Ta5d6ff Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6fftogglePlayTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3:icon-name=Ta5d6ff"Ta5d6ffisPlaying ? Ta5d6ff'Ta5d6ffpauseTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffplayTa5d6ff'Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
{{ isPlaying ? "Pause Selection" : "Play Selection" }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>

T8b949e<!-- Footer -->
Tff7b72<div
Te6edf3class=Ta5d6ff"Ta5d6ffpx-6 py-4 bg-gray-50 dark:bg-zinc-800/30 border-t border-gray-100 dark:border-zinc-800 flex items-center justify-betweenTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-2Ta5d6ff"Tff7b72>
Tff7b72<input
Te6edf3id=Ta5d6ff"Ta5d6ffsaveAsNewTa5d6ff"
Te6edf3v-model=Ta5d6ff"Ta5d6ffsaveAsNewTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffcheckboxTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded border-gray-300 text-blue-600 focus:ring-blue-500Ta5d6ff"
Tff7b72/>
Tff7b72<label Te6edf3for=Ta5d6ff"Ta5d6ffsaveAsNewTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6fftext-sm text-gray-600 dark:text-zinc-400 cursor-pointerTa5d6ff"
Tff7b72>Save as new ringtoneTff7b72</label
>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-3Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3class=Ta5d6ff"Ta5d6ffpx-4 py-2 text-sm font-bold text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300 transition-colorsTa5d6ff"
@Te6edf3click=Ta5d6ff"Te6edf3$emitTa5d6ff(Ta5d6ff'Ta5d6ffcloseTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Cancel
Tff7b72</button>
Tff7b72<button
Te6edf3:disabled=Ta5d6ff"Ta5d6ffsaving || loadingTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffpx-6 py-2 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-xl text-sm font-bold shadow-lg transition-all hover:scale-[1.02] active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsaveTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3v-if=Ta5d6ff"Ta5d6ffsavingTa5d6ff" Te6edf3icon-name=Ta5d6ff"Ta5d6ffloadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 animate-spinTa5d6ff" Tff7b72/>
{{ saving ? "Saving..." : "Save Audio" }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</template>

Tff7b72<scriptTff7b72>
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import ToastUtils from "../../js/ToastUtils";

export default {
name: "RingtoneEditorModal",
components: {
MaterialDesignIcon,
},
props: {
ringtone: {
type: Object,
required: true,
},
},
emits: ["close", "saved"],
data() {
return {
loading: true,
saving: false,
audioBuffer: null,
audioContext: null,
sourceNode: null,
startTime: 0,
endTime: 0,
totalDuration: 0,
isPlaying: false,
playbackStartTime: 0,
playbackStartOffset: 0,
progressPercent: 0,
animationFrame: null,
dragging: null,
saveAsNew: false,
};
},
computed: {
startPercent() {
return (this.startTime / this.totalDuration) * 100 || 0;
},
endPercent() {
return (this.endTime / this.totalDuration) * 100 || 100;
},
},
mounted() {
this.loadAudio();
window.addEventListener("mousemove", this.handleDragging);
window.addEventListener("mouseup", this.stopDragging);
window.addEventListener("resize", this.drawWaveform);
},
beforeUnmount() {
this.stopPlayback();
window.removeEventListener("mousemove", this.handleDragging);
window.removeEventListener("mouseup", this.stopDragging);
window.removeEventListener("resize", this.drawWaveform);
if (this.audioContext) {
this.audioContext.close();
}
},
methods: {
async loadAudio() {
try {
this.loading = true;
const response = await fetch(`/api/v1/telephone/ringtones/${this.ringtone.id}/audio`);
const arrayBuffer = await response.arrayBuffer();

this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
this.audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);

this.totalDuration = this.audioBuffer.duration;
this.startTime = 0;
this.endTime = this.totalDuration;

this.$nextTick(() => {
this.drawWaveform();
});
} catch (e) {
console.error("Failed to load audio:", e);
ToastUtils.error(this.$t("call.failed_load_audio_edit"));
this.$emit("close");
} finally {
this.loading = false;
}
},
drawWaveform() {
const canvas = this.$refs.waveform;
if (!canvas || !this.audioBuffer) return;

const ctx = canvas.getContext("2d");
const width = canvas.clientWidth;
const height = canvas.clientHeight;

// Set canvas internal resolution
canvas.width = width * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);

const data = this.audioBuffer.getChannelData(0);
const step = Math.ceil(data.length / width);
const amp = height / 2;

ctx.clearRect(0, 0, width, height);
ctx.beginPath();
ctx.moveTo(0, amp);

// Draw a nice modern waveform
ctx.strokeStyle = this.isDarkMode() ? "#3f3f46" : "#e4e4e7";
ctx.lineWidth = 1;

for (let i = 0; i Tff7b72< width; i++) {
let Te6edf3min = Ta5d6ff1.0;
let Te6edf3max = Ta5d6ff-1.0;
for (let Te6edf3j = Ta5d6ff0; j < step; j++) {
const Te6edf3datum = Ta5d6ffdata[i * step + j];
if (datum < min) Te6edf3min = Ta5d6ffdatum;
if (datum Tff7b72> max) max = datum;
}
ctx.moveTo(i, amp + min * amp);
ctx.lineTo(i, amp + max * amp);
}
ctx.stroke();

// Highlight the selected range
const startX = (this.startTime / this.totalDuration) * width;
const endX = (this.endTime / this.totalDuration) * width;

ctx.beginPath();
ctx.strokeStyle = "#3b82f6";
ctx.lineWidth = 1.5;
for (let i = Math.floor(startX); i Tff7b72< Math.ceil(endX); i++) {
let Te6edf3min = Ta5d6ff1.0;
let Te6edf3max = Ta5d6ff-1.0;
for (let Te6edf3j = Ta5d6ff0; j < step; j++) {
const Te6edf3datum = Ta5d6ffdata[i * step + j];
if (datum < min) Te6edf3min = Ta5d6ffdatum;
if (datum Tff7b72> max) max = datum;
}
ctx.moveTo(i, amp + min * amp);
ctx.lineTo(i, amp + max * amp);
}
ctx.stroke();
},
isDarkMode() {
return document.documentElement.classList.contains("dark");
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = (seconds % 60).toFixed(2);
return `${mins}:${secs.padStart(5, "0")}`;
},
startDragging(type) {
this.dragging = type;
},
stopDragging() {
this.dragging = null;
this.drawWaveform();
},
handleDragging(e) {
if (!this.dragging) return;

const canvas = this.$refs.waveform;
const rect = canvas.getBoundingClientRect();
const x = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
const time = (x / rect.width) * this.totalDuration;

if (this.dragging === "start") {
this.startTime = Math.min(time, this.endTime - 0.1);
} else if (this.dragging === "end") {
this.endTime = Math.max(time, this.startTime + 0.1);
}
},
handleWaveformClick(e) {
const canvas = this.$refs.waveform;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const time = (x / rect.width) * this.totalDuration;

// If click is outside selection, move nearest boundary
if (time Tff7b72< this.startTime) {
Te6edf3this.startTime = Ta5d6fftime;
} else if (time Tff7b72> this.endTime) {
this.endTime = time;
} else {
// If click is inside, maybe we can use it to seek preview
// but for now let's just keep it simple
}
this.drawWaveform();
},
togglePlay() {
if (this.isPlaying) {
this.stopPlayback();
} else {
this.startPlayback();
}
},
startPlayback() {
if (!this.audioBuffer) return;

this.stopPlayback();

this.sourceNode = this.audioContext.createBufferSource();
this.sourceNode.buffer = this.audioBuffer;
this.sourceNode.connect(this.audioContext.destination);

this.playbackStartOffset = this.startTime;
this.playbackStartTime = this.audioContext.currentTime;

this.sourceNode.start(0, this.startTime, this.endTime - this.startTime);
this.isPlaying = true;

this.sourceNode.onended = () => {
this.isPlaying = false;
this.progressPercent = 0;
cancelAnimationFrame(this.animationFrame);
};

this.updateProgress();
},
stopPlayback() {
if (this.sourceNode) {
this.sourceNode.stop();
this.sourceNode = null;
}
this.isPlaying = false;
this.progressPercent = 0;
cancelAnimationFrame(this.animationFrame);
},
updateProgress() {
if (!this.isPlaying) return;

const elapsed = this.audioContext.currentTime - this.playbackStartTime;
const currentTime = this.playbackStartOffset + elapsed;
this.progressPercent = (currentTime / this.totalDuration) * 100;

if (currentTime >= this.endTime) {
this.stopPlayback();
return;
}

this.animationFrame = requestAnimationFrame(this.updateProgress);
},
async save() {
try {
this.saving = true;

// Create a trimmed version of the audio
const sampleRate = this.audioBuffer.sampleRate;
const startSample = Math.floor(this.startTime * sampleRate);
const endSample = Math.floor(this.endTime * sampleRate);
const frameCount = endSample - startSample;

const offlineCtx = new OfflineAudioContext(this.audioBuffer.numberOfChannels, frameCount, sampleRate);

const trimmedBuffer = offlineCtx.createBuffer(
this.audioBuffer.numberOfChannels,
frameCount,
sampleRate
);

for (let channel = 0; channel Tff7b72< this.audioBuffer.numberOfChannels; channel++) {
const Te6edf3data = Ta5d6ffthis.audioBuffer.getChannelData(channel);
const Te6edf3trimmedData = Ta5d6fftrimmedBuffer.getChannelData(channel);
for (let Te6edf3i = Ta5d6ff0; i < frameCount; i++) {
trimmedData[i] = data[startSample + i];
}
}

// Convert AudioBuffer to WAV blob
const Te6edf3blob = Ta5d6ffthis.audioBufferToWav(trimmedBuffer);

const Te6edf3formData = Ta5d6ffnew FormData();
const Te6edf3filename = Ta5d6ffthis.saveAsNew ? `edited_${this.ringtone.filename}` : this.ringtone.filename;

formData.append("file", blob, filename);

if (this.saveAsNew) {
await window.api.post("/api/v1/telephone/ringtones/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});
} else {
// We don't have a direct "replace" endpoint, but we can delete and upload,
// or just upload as a new one.
// For now let's just upload as a new one if it's simpler,
// but the user might expect replacement.
// The backend doesn't seem to support direct replacement via the upload endpoint.
// Let's just upload it.
await window.api.post("/api/v1/telephone/ringtones/upload", formData, {
headers: { "Content-Type": "multipart/form-data" },
});

// If not saving as new, maybe delete the old one?
// await window.api.delete(`/api/v1/telephone/ringtones/${this.ringtone.id}`);
}

ToastUtils.success(this.$t("call.ringtone_saved"));
this.$emit("saved");
this.$emit("close");
} catch (e) {
console.error("Failed to save ringtone:", e);
ToastUtils.error(this.$t("call.failed_save_ringtone"));
} finally {
Te6edf3this.saving = Ta5d6fffalse;
}
},
audioBufferToWav(buffer) {
const Te6edf3numOfChan = Ta5d6ffbuffer.numberOfChannels;
const Te6edf3length = Ta5d6ffbuffer.length * numOfChan * 2 + 44;
const Te6edf3buffer_arr = Ta5d6ffnew ArrayBuffer(length);
const Te6edf3view = Ta5d6ffnew DataView(buffer_arr);
const Te6edf3channels = Ta5d6ff[];
let i;
let sample;
let Te6edf3offset = Ta5d6ff0;
let Te6edf3pos = Ta5d6ff0;

const Te6edf3setUint16 = Ta5d6ff(data) =Tff7b72> {
view.setUint16(pos, data, true);
pos += 2;
};

const setUint32 = (data) => {
view.setUint32(pos, data, true);
pos += 4;
};

const writeString = (string) => {
for (let i = 0; i Tff7b72< string.length; i++) {
view.setUint8(pos + i, string.charCodeAt(i));
}
pos += string.length;
};

writeString("RIFF");
setUint32(length - 8);
writeString("WAVE");
writeString("fmt ");
setUint32(16);
setUint16(1);
setUint16(numOfChan);
setUint32(buffer.sampleRate);
setUint32(buffer.sampleRate * 2 * numOfChan);
setUint16(numOfChan * 2);
setUint16(16);
writeString("data");
setUint32(length - pos - 4);

for (Te6edf3i = Ta5d6ff0; i < buffer.numberOfChannels; i++) channels.push(buffer.getChannelData(i));

while (pos < length) {
for (Te6edf3i = Ta5d6ff0; i < numOfChan; i++) {
Te6edf3sample = Ta5d6ffMath.max(-1, Math.min(1, channels[i][offset]));
Te6edf3sample = Ta5d6ff(sample < 0 ? sample * 0x8000 : sample * 0x7fff) | 0;
view.setInt16(pos, sample, true);
pos += 2;
}
offset++;
}

return new Blob([buffer_arr], { type: "audio/wav" });
},
},
};
</scriptTff7b72>

Tff7b72<style scopedTff7b72>
canvas {
image-rendering: pixelated;
}
Tff7b72</style>

Served by rngit 1.5.2 - Generated in 0.04s